iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

Java × AI:30天打造智慧健康助理系列 第 19 篇

用 Java 建立七天睡眠統計 API

  • 分享至 

  • xImage
  •  

Day 19|用 Java 建立七天睡眠統計 API

昨天的 Day 18,我們成功讓智慧健康助理加入睡眠時間紀錄功能!

現在除了飲水量之外,也可以透過 Spring Boot API 新增睡眠紀錄,並將資料保存到 H2 Database。

例如:

{
  "sleepDate": "2026-10-17",
  "hours": 7.5
}

不過目前只能新增紀錄和查詢指定日期。

如果想知道自己最近一週平均睡了多久,還是得把每天的數字拿出來自己計算。

既然前幾天已經幫飲水量建立統計功能,今天當然也要讓睡眠紀錄跟上進度 XD

所以今天決定挑戰:

建立七天睡眠時間統計 API!

今天的目標是:

使用 Spring JDBC、SQL 和 Java,計算最近七天的平均睡眠時數,並找出最長與最短的睡眠紀錄。


一、今天要完成哪些功能?

目前睡眠紀錄已經有兩個 API:

HTTP Method API 功能
POST /health/sleep 新增睡眠紀錄
GET /health/sleep?date=2026-10-17 查詢指定日期紀錄

今天準備新增:

GET /health/sleep/statistics

讓使用者可以直接取得最近七天的睡眠統計。

例如:

{
  "totalHours": 49.5,
  "averageHours": 7.07,
  "maxHours": 8.5,
  "maxDate": "2026-10-15",
  "minHours": 6.0,
  "minDate": "2026-10-12",
  "recordedDays": 7,
  "periodDays": 7,
  "unit": "hours"
}

這樣就可以一次看到:

  • 七天累積睡眠紀錄時數
  • 七天平均睡眠時數
  • 最長睡眠紀錄
  • 最短睡眠紀錄
  • 有紀錄的天數

不過今天的統計只會整理使用者輸入的睡眠時數,不會直接判斷睡眠品質。


二、先確認睡眠統計的計算方式

在開始寫程式之前,我們先思考一個問題。

如果同一天有兩筆睡眠紀錄,應該怎麼計算?

例如:

10/17 晚上主要睡眠:7 小時
10/17 下午補眠:1 小時

昨天建立的資料庫允許同一天新增多筆紀錄。

所以今天決定先採用:

將同一天的睡眠紀錄加總,作為當日總睡眠時數。

例如:

7 + 1 = 8 小時

不過要注意,這個計算方式假設同一天的紀錄沒有重複輸入,而且各筆睡眠時段沒有重疊。

目前系統尚未記錄實際入睡與起床時間,所以還無法自動檢查重複時段。

另外,如果某一天沒有紀錄,我們也不能直接認定使用者完全沒有睡覺。

因此今天會區分:

七天平均紀錄時數

以及:

有紀錄的天數

避免把沒有資料誤認成實際睡眠時數為零。


三、回顧目前的資料表

昨天已經建立:

sleep_records

資料表結構如下:

CREATE TABLE IF NOT EXISTS sleep_records (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    sleep_date DATE NOT NULL,
    hours DOUBLE PRECISION NOT NULL,
    CONSTRAINT chk_sleep_hours
        CHECK (hours > 0 AND hours <= 24)
);

假設資料庫裡有:

id sleep_date hours
1 2026-10-15 7.0
2 2026-10-16 6.5
3 2026-10-17 7.0
4 2026-10-17 1.0

其中 10/17 有兩筆紀錄。

如果我們想知道每天的總睡眠時數,就需要將同一天的資料加總。

這時候又可以用到前幾天學過的 SQL:

GROUP BY

四、使用 SQL 計算每日睡眠時數

首先在 H2 Console 試著執行:

SELECT
    sleep_date,
    SUM(hours) AS total_hours
FROM sleep_records
GROUP BY sleep_date
ORDER BY sleep_date;

這段 SQL 的意思是:

  1. 依照睡眠日期分組。
  2. 將同一天的睡眠時數加總。
  3. 依照日期排序。

假設原本的資料是:

10/15 → 7.0 小時
10/16 → 6.5 小時
10/17 → 7.0 小時
10/17 → 1.0 小時

查詢結果就會變成:

10/15 → 7.0 小時
10/16 → 6.5 小時
10/17 → 8.0 小時

這樣就能把多筆睡眠紀錄整理成每日統計資料。


五、建立 SleepHistoryResponse DTO

接下來要讓 Java 接收 SQL 查詢結果。

所以今天先建立:

SleepHistoryResponse.java

放在:

dto

Package 裡面。

完整程式如下:

package com.example.healthassistant.dto;

import java.time.LocalDate;

public class SleepHistoryResponse {

    private LocalDate date;
    private double totalHours;

    public SleepHistoryResponse(
            LocalDate date,
            double totalHours) {

        this.date = date;
        this.totalHours = totalHours;
    }

    public LocalDate getDate() {
        return date;
    }

    public double getTotalHours() {
        return totalHours;
    }
}

這個 DTO 用來表示某一天的睡眠總時數。

例如:

{
  "date": "2026-10-17",
  "totalHours": 8.0
}

和昨天的 SleepResponse 不同。

昨天的 SleepResponse 代表單筆睡眠紀錄。

今天的 SleepHistoryResponse 則代表同一天加總後的結果。


六、修改 SleepRepository

接下來打開:

SleepRepository.java

加入一個新的 Method:

public List<SleepHistoryResponse> findHistory(
        LocalDate startDate,
        LocalDate endDate) {

    String sql = """
        SELECT
            sleep_date,
            SUM(hours) AS total_hours
        FROM sleep_records
        WHERE sleep_date BETWEEN ? AND ?
        GROUP BY sleep_date
        ORDER BY sleep_date
        """;

    return jdbcTemplate.query(
        sql,
        (rs, rowNum) -> new SleepHistoryResponse(
            rs.getDate("sleep_date").toLocalDate(),
            rs.getDouble("total_hours")
        ),
        startDate,
        endDate
    );
}

記得加入:

import com.example.healthassistant.dto.SleepHistoryResponse;

這裡和 Day 16 的飲水量歷史查詢非常相似。

我們使用:

BETWEEN ? AND ?

查詢指定日期範圍。

再使用:

SUM(hours)

計算每日總睡眠時數。

最後透過:

jdbcTemplate.query()

將查詢結果轉換成:

List<SleepHistoryResponse>

這樣 Repository 就可以提供最近七天的睡眠紀錄給 Service。


七、建立 SleepStatisticsResponse DTO

接下來要建立統計結果的 DTO。

新增:

SleepStatisticsResponse.java

完整程式如下:

package com.example.healthassistant.dto;

import java.time.LocalDate;

public class SleepStatisticsResponse {

    private double totalHours;
    private double averageHours;

    private double maxHours;
    private LocalDate maxDate;

    private double minHours;
    private LocalDate minDate;

    private int recordedDays;
    private int periodDays;

    private String unit;

    public SleepStatisticsResponse(
            double totalHours,
            double averageHours,
            double maxHours,
            LocalDate maxDate,
            double minHours,
            LocalDate minDate,
            int recordedDays,
            int periodDays) {

        this.totalHours = totalHours;
        this.averageHours = averageHours;

        this.maxHours = maxHours;
        this.maxDate = maxDate;

        this.minHours = minHours;
        this.minDate = minDate;

        this.recordedDays = recordedDays;
        this.periodDays = periodDays;

        this.unit = "hours";
    }

    public double getTotalHours() {
        return totalHours;
    }

    public double getAverageHours() {
        return averageHours;
    }

    public double getMaxHours() {
        return maxHours;
    }

    public LocalDate getMaxDate() {
        return maxDate;
    }

    public double getMinHours() {
        return minHours;
    }

    public LocalDate getMinDate() {
        return minDate;
    }

    public int getRecordedDays() {
        return recordedDays;
    }

    public int getPeriodDays() {
        return periodDays;
    }

    public String getUnit() {
        return unit;
    }
}

這個 DTO 會負責回傳統計結果。

欄位 說明
totalHours 七天累積紀錄時數
averageHours 七天平均紀錄時數
maxHours 單日最長紀錄時數
maxDate 最長紀錄日期
minHours 單日最短紀錄時數
minDate 最短紀錄日期
recordedDays 有紀錄的天數
periodDays 統計期間天數
unit 時數單位

八、使用 Java 計算睡眠總時數

接下來進入今天最重要的部分。

打開:

SleepService.java

我們要新增:

getSleepStatistics()

首先取得最近七天的紀錄:

LocalDate today = LocalDate.now();

LocalDate startDate = today.minusDays(6);

List<SleepHistoryResponse> history =
        sleepRepository.findHistory(startDate, today);

和之前的飲水量統計一樣。

因為我們希望查詢:

包含今天在內的最近七個日曆日。

所以使用:

minusDays(6)

接著計算總睡眠時數:

double total = 0;

for (SleepHistoryResponse record : history) {
    total += record.getTotalHours();
}

例如:

7.0 + 6.5 + 8.0 = 21.5

這樣就能得到累積睡眠時數。


九、計算七天平均睡眠時數

接下來要計算平均值。

假設最近七天的睡眠紀錄如下:

日期 睡眠時數
10/11 7.0
10/12 6.0
10/13 7.5
10/14 8.0
10/15 8.5
10/16 6.5
10/17 6.0

總時數:

7 + 6 + 7.5 + 8 + 8.5 + 6.5 + 6
= 49.5 小時

平均值:

49.5 ÷ 7 ≈ 7.07 小時

在 Java 裡面可以寫:

double average = total / 7.0;

再使用:

double roundedAverage =
        Math.round(average * 100.0) / 100.0;

保留小數點後兩位。

這樣就完成七天平均睡眠時數計算。

不過這裡的平均值是以七個日曆日為分母。

如果某一天沒有紀錄,該日不會增加總時數,但仍然包含在七天期間內。

所以這個數字應該理解為:

七天平均記錄睡眠時數,而不是實際平均睡眠時間。


十、找出最長與最短睡眠紀錄

接下來要找出:

最長睡眠紀錄

以及:

最短睡眠紀錄

和昨天的飲水統計類似,我們可以使用:

if

進行比較。

例如:

if (hours > maxHours) {
    maxHours = hours;
    maxDate = record.getDate();
}

如果目前的睡眠時數大於最高值,就更新最高紀錄。

最低紀錄也是相同概念:

if (hours < minHours) {
    minHours = hours;
    minDate = record.getDate();
}

不過如果最近七天完全沒有紀錄,就不能直接假設有一筆 0 小時的睡眠資料。

因此今天同樣會處理沒有資料的情況。


十一、完整的睡眠統計程式

現在把剛剛的邏輯整理成一個完整 Method。

在 SleepService.java 加入:

public SleepStatisticsResponse getSleepStatistics() {

    LocalDate today = LocalDate.now();

    LocalDate startDate = today.minusDays(6);

    List<SleepHistoryResponse> history =
            sleepRepository.findHistory(startDate, today);

    double total = 0;

    double maxHours = 0;
    double minHours = 0;

    LocalDate maxDate = null;
    LocalDate minDate = null;

    boolean firstRecord = true;

    for (SleepHistoryResponse record : history) {

        double hours = record.getTotalHours();

        total += hours;

        if (firstRecord) {

            maxHours = hours;
            minHours = hours;

            maxDate = record.getDate();
            minDate = record.getDate();

            firstRecord = false;

        } else {

            if (hours > maxHours) {
                maxHours = hours;
                maxDate = record.getDate();
            }

            if (hours < minHours) {
                minHours = hours;
                minDate = record.getDate();
            }
        }
    }

    double average = total / 7.0;

    double roundedTotal =
            Math.round(total * 100.0) / 100.0;

    double roundedAverage =
            Math.round(average * 100.0) / 100.0;

    return new SleepStatisticsResponse(
        roundedTotal,
        roundedAverage,
        maxHours,
        maxDate,
        minHours,
        minDate,
        history.size(),
        7
    );
}

記得在 SleepService.java 加入:

import com.example.healthassistant.dto.SleepHistoryResponse;
import com.example.healthassistant.dto.SleepStatisticsResponse;

這段程式的流程如下:

取得最近七天日期
        ↓
查詢 H2 Database
        ↓
取得每日睡眠時數
        ↓
計算總時數
        ↓
計算平均時數
        ↓
找出最長紀錄
        ↓
找出最短紀錄
        ↓
回傳統計 DTO

如果最高或最低值有多個日期相同,目前會保留查詢順序中最早出現的日期。

因為 SQL 已經使用:

ORDER BY sleep_date

所以結果會依照日期由舊到新排列。


十二、建立睡眠統計 API

Service 完成後,接下來要建立 API。

打開:

SleepController.java

加入:

@GetMapping("/statistics")
public ResponseEntity<SleepStatisticsResponse> getStatistics() {

    SleepStatisticsResponse statistics =
            sleepService.getSleepStatistics();

    return ResponseEntity.ok(statistics);
}

記得加入:

import com.example.healthassistant.dto.SleepStatisticsResponse;

因為 Controller 已經設定:

@RequestMapping("/health/sleep")

所以完整 API 路徑就是:

GET /health/sleep/statistics

這樣就完成今天的新功能!


十三、使用 Postman 測試

接下來重新啟動 Spring Boot。

HTTP Method:

GET

URL:

http://localhost:8080/health/sleep/statistics

這次不需要傳送 Request Body。

假設最近七天有以下資料:

日期 睡眠時數
10/11 7.0
10/12 6.0
10/13 7.5
10/14 8.0
10/15 8.5
10/16 6.5
10/17 6.0

預期回傳:

{
  "totalHours": 49.5,
  "averageHours": 7.07,
  "maxHours": 8.5,
  "maxDate": "2026-10-15",
  "minHours": 6.0,
  "minDate": "2026-10-12",
  "recordedDays": 7,
  "periodDays": 7,
  "unit": "hours"
}

這樣就能一次取得最近七天的睡眠統計。

實際回傳結果會依照資料庫中的紀錄而有所不同。


十四、如果最近七天沒有睡眠紀錄呢?

接下來考慮一個特殊情況。

如果資料庫裡完全沒有最近七天的睡眠紀錄,會發生什麼事?

因為:

history.size()

等於:

0

所以:

total

會維持:

0

而:

maxDate

和:

minDate

則會維持:

null

因此預期回傳:

{
  "totalHours": 0.0,
  "averageHours": 0.0,
  "maxHours": 0.0,
  "maxDate": null,
  "minHours": 0.0,
  "minDate": null,
  "recordedDays": 0,
  "periodDays": 7,
  "unit": "hours"
}

這樣程式就不會因為沒有資料而發生錯誤。

不過如果未來要建立前端介面,最好另外顯示:

最近七天尚無睡眠紀錄。

避免使用者誤以為自己真的睡了 0 小時。


十五、目前的專案架構

經過今天的更新,專案又多了兩個 DTO。

目前的結構如下:

health-assistant
│
├── src/main/java/com/example/healthassistant
│   │
│   ├── HealthAssistantApplication.java
│   │
│   ├── controller
│   │   ├── HealthController.java
│   │   ├── WaterController.java
│   │   └── SleepController.java
│   │
│   ├── service
│   │   ├── BmiService.java
│   │   ├── WaterService.java
│   │   └── SleepService.java
│   │
│   ├── repository
│   │   ├── WaterRepository.java
│   │   └── SleepRepository.java
│   │
│   ├── dto
│   │   ├── HealthRequest.java
│   │   ├── WaterRequest.java
│   │   ├── WaterHistoryResponse.java
│   │   ├── WaterStatisticsResponse.java
│   │   ├── SleepRequest.java
│   │   ├── SleepResponse.java
│   │   ├── SleepHistoryResponse.java
│   │   └── SleepStatisticsResponse.java
│   │
│   └── exception
│       └── GlobalExceptionHandler.java
│
├── src/main/resources
│   ├── application.properties
│   └── schema.sql
│
└── pom.xml

現在系統已經有兩種統計功能:

飲水量統計
    ↓
WaterService
    ↓
WaterRepository
    ↓
H2 Database

以及:

睡眠時間統計
    ↓
SleepService
    ↓
SleepRepository
    ↓
H2 Database

這樣未來如果想加入更多健康資料,也可以沿用相似的架構。


十六、今天為什麼還不需要 AI?

今天完成的功能主要是:

加總
平均
最大值
最小值

這些都有明確的計算方式。

所以直接使用 Java 就可以完成。

例如:

double average = total / 7.0;

就能計算平均睡眠時數。

不需要把所有資料丟給 AI,請它幫忙算數學。

我覺得這也是這個專案很重要的一個觀念:

不是所有功能都需要 AI。

Java 比較適合負責:

  • 接收使用者輸入
  • 驗證資料
  • 儲存健康紀錄
  • 查詢歷史資料
  • 計算統計結果

而 AI 未來可以負責:

  • 將統計結果整理成自然語言
  • 產生容易閱讀的生活紀錄摘要
  • 回答一般性的健康資訊問題

這樣分工比較清楚,也能減少不必要的 AI 呼叫。


十七、睡眠統計有哪些限制?

雖然今天已經完成七天睡眠統計,但目前仍然有幾個限制。

1. 沒有紀錄不等於沒有睡覺

如果某一天沒有輸入睡眠時數,系統就不會有那一天的資料。

所以七天平均紀錄量可能低於實際平均睡眠時間。

2. 睡眠時數不代表睡眠品質

即使兩個人都睡了八小時,也不代表他們的睡眠品質相同。

目前系統沒有記錄夜間醒來次數、睡眠中斷或其他資訊。

因此不能直接用睡眠時數判斷睡眠品質。

3. 同一天的多筆紀錄會加總

目前我們使用:

SUM(hours)

將同一天的睡眠時數加總。

如果使用者重複輸入相同紀錄,就可能造成統計結果偏高。

未來可以加入修改、刪除及避免重複輸入的功能。

4. 目前沒有個人化分析

我們只計算紀錄中的數字。

不會直接根據睡眠時數提供醫療診斷或個人化治療建議。


Day 19 小結

今天成功讓智慧健康助理加入睡眠統計功能!

現在除了飲水量之外,睡眠紀錄也可以進行基本的資料分析。

今天完成的功能包括:

  1. 使用 SQL 查詢最近七天的睡眠紀錄
  2. 使用 GROUP BY 計算每日睡眠總時數
  3. 建立 SleepHistoryResponse DTO
  4. 建立 SleepStatisticsResponse DTO
  5. 使用 Java 計算七天平均睡眠時數
  6. 找出最長與最短的睡眠紀錄
  7. 建立睡眠統計 API

目前的智慧健康助理已經具備:

智慧健康助理
│
├── BMI 計算
│   └── BMI 分類
│
├── 飲水量紀錄
│   ├── 新增紀錄
│   ├── 歷史查詢
│   └── 七天統計
│
└── 睡眠時間紀錄
    ├── 新增紀錄
    ├── 指定日期查詢
    └── 七天統計

今天最大的收穫是:

將相同的資料分析概念,應用到不同種類的健康紀錄上。

經過這幾天的練習,我開始發現,建立一個系統不一定要每天學習全新的技術。

有時候更重要的是把已經學過的技術,應用到不同的功能中。


明日預告

Day 20|整合健康資料!建立每日健康摘要 API

目前我們已經完成:

BMI 計算
飲水量紀錄
睡眠時間紀錄

而且飲水量和睡眠時間都有自己的統計功能。

不過這些資料目前分散在不同 API。

如果使用者想一次查看今天的健康紀錄,就需要分別呼叫不同的 API。

所以下一篇準備挑戰:

建立每日健康摘要 API!

預計會學習:

  • 如何整合不同 Service 的資料
  • 建立 HealthSummaryResponse DTO
  • 整合每日飲水量與睡眠紀錄
  • 建立統一的健康摘要 API
  • 為後續 AI 文字摘要功能做準備

讓智慧健康助理開始從單一功能,逐漸變成可以整合不同健康資訊的系統!


上一篇
用 Java 建立睡眠時間紀錄功能
下一篇
用 Spring Boot 建立每日健康摘要 API
系列文
Java × AI:30天打造智慧健康助理 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言